/* Copyright (C) 2008  Romolo Camplani, Marco Fortunato, Marco Marelli, Guido Rota, Fabio A. Schreiber et al.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package org.dei.perla.sys.device.fpc.dspic;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;

import org.dei.perla.parser.clauses.OnUnsupportedSampleRateOptions;
import org.dei.perla.parser.expressions.*;
import org.dei.perla.parser.util.PairValue;
import org.dei.perla.sys.device.fpc.FPC;
import org.dei.perla.sys.device.fpc.dspic.datastructures.DSPicOnContinuousStatus;
import org.dei.perla.sys.device.fpc.dspic.datastructures.DSPicOnEventStatus;
import org.dei.perla.sys.device.fpc.dspic.datastructures.ED;
import org.dei.perla.sys.device.fpc.dspic.datastructures.MRP;
import org.dei.perla.sys.llqruntime.datastructures.QueryRecordStructure;
import org.dei.perla.sys.llqruntime.datastructures.Record;
import org.dei.perla.utils.dataconverter.ArrayUtility;
import org.dei.perla.utils.dataconverter.DataConverter;
import org.dei.perla.utils.dataconverter.annotations.StructInfo;
import org.dei.perla.utils.messages.AdapterFpcMessage;
import org.dei.perla.utils.pipe.Pipe;
import org.dei.perla.utils.pipe.PipePair;
import org.dei.perla.utils.waiter.Waitable;
import org.dei.perla.utils.waiter.Waiter;

public class DSPicFPC extends FPC {
	
	public static final int ON_EVENT = 0;
	public static final int CONTINUOUS = 1;
	
	private final Pipe<AdapterFpcMessage> pFpcPipeIn;
	private final Pipe<AdapterFpcMessage> pFpcPipeOut;
	private final Pipe<ArrayList<PairValue<String, ? extends Constant>>> pDataCollectorPipeIn;
	private final ArrayList<Pipe<Record>> pDataCollectorPipeOut;
	private final Thread pFpcListener;
	private final Waiter<Pipe<AdapterFpcMessage>> pFpcPipeInWaiter;
	private QueryRecordStructure continuousRecordStructure;
	private QueryRecordStructure onEventRecordStructure;
	private MRP pMRP;
	private final int pDevNumber;
	private int acquisitionType;
	private final int sizeOnEvent;
	private final int sizeOnContinuous;
	
	
	public DSPicFPC(PipePair<AdapterFpcMessage, AdapterFpcMessage> parPipePair, int parDevNumber) {
		StructInfo structInfo;
		pDataCollectorPipeOut = new ArrayList<Pipe<Record>>();
		pDataCollectorPipeIn = new Pipe<ArrayList<PairValue<String, ? extends Constant>>>(
				"DSPicFPC " + parDevNumber + " DataCollectorPipeIn");
		pDataCollectorPipeIn.start();
		
		pFpcPipeIn = parPipePair.getInPipe();
		pFpcPipeOut = parPipePair.getOutPipe();
		pFpcPipeInWaiter = new Waiter<Pipe<AdapterFpcMessage>>();
		pFpcPipeInWaiter.addWaitable(pFpcPipeIn.getWaitable());
		pFpcPipeInWaiter.start();
		pDevNumber = parDevNumber;
		
		structInfo = DSPicOnContinuousStatus.class.getAnnotation(StructInfo.class);
		sizeOnContinuous = structInfo.totalStructSize();
		structInfo = DSPicOnEventStatus.class.getAnnotation(StructInfo.class);
		sizeOnEvent = structInfo.totalStructSize();
		// Imposto lo stato iniziale di sampling
		
		acquisitionType = ON_EVENT;
		
		// Inserisco i valori di default nella struttura dei parametri da aggiornare
		pMRP = new MRP();
		pMRP.setAcquisitionType(1);
		pMRP.setCommand(1);
		pMRP.setRs1(2);
		pMRP.setRs2(3);
		pMRP.setRs3(4);
		pMRP.setRs4(5);
		pMRP.setGx(65);
		pMRP.setGy(66);
		pMRP.setGz(67);
		pMRP.setThupX(9);
		pMRP.setThupY(10);
		pMRP.setThupZ(11);
		pMRP.setThdwX(12);
		pMRP.setThdwY(13);
		pMRP.setThdwZ(14);
		int[] thupsValues = new int[64];
		Arrays.fill(thupsValues, 0);
		pMRP.setTaps(thupsValues);
		
		// Creo la struttura del record per il campionamento continuo
		continuousRecordStructure = new QueryRecordStructure();
		continuousRecordStructure.addFieldStructure("ID_NODO", ConstantID.class);
		continuousRecordStructure.addFieldStructure("ACQUISITION_TYPE", ConstantInteger.class);
		continuousRecordStructure.addFieldStructure("DATA", ConstantBlob.class);
		continuousRecordStructure.addFieldStructure("TIMESTAMP", ConstantTimestamp.class);
		continuousRecordStructure.addFieldStructure("TEMPERATURE", ConstantInteger.class);
		continuousRecordStructure.addFieldStructure("ATTITUDEX", ConstantInteger.class);
		continuousRecordStructure.addFieldStructure("ATTITUDEY", ConstantInteger.class);
		continuousRecordStructure.addFieldStructure("ATTITUDEZ", ConstantInteger.class);
		continuousRecordStructure.addFieldStructure("CRACKMETER1", ConstantInteger.class);
		continuousRecordStructure.addFieldStructure("CRACKMETER2", ConstantInteger.class);
		
		// Creo la struttura del record per il campionamento ad eventi
		onEventRecordStructure = new QueryRecordStructure();
		onEventRecordStructure.addFieldStructure("ID_NODO", ConstantID.class);
		onEventRecordStructure.addFieldStructure("ACQUISITION_TYPE", ConstantInteger.class);
		onEventRecordStructure.addFieldStructure("NUM_EVENTS", ConstantInteger.class);
		onEventRecordStructure.addFieldStructure("FLAGS", ConstantInteger.class);
		onEventRecordStructure.addFieldStructure("SAMPLES", ConstantInteger.class);
		onEventRecordStructure.addFieldStructure("MAGNITUDEX", ConstantInteger.class);
		onEventRecordStructure.addFieldStructure("MAGNITUDEY", ConstantInteger.class);
		onEventRecordStructure.addFieldStructure("MAGNITUDEZ", ConstantInteger.class);
		onEventRecordStructure.addFieldStructure("PEAKX", ConstantInteger.class);
		onEventRecordStructure.addFieldStructure("PEAKY", ConstantInteger.class);
		onEventRecordStructure.addFieldStructure("PEAKZ", ConstantInteger.class);
		onEventRecordStructure.addFieldStructure("NOISEX", ConstantInteger.class);
		onEventRecordStructure.addFieldStructure("NOISEY", ConstantInteger.class);
		onEventRecordStructure.addFieldStructure("NOISEZ", ConstantInteger.class);
		onEventRecordStructure.addFieldStructure("DATA", ConstantBlob.class);
		onEventRecordStructure.addFieldStructure("TIMESTAMP", ConstantTimestamp.class);
		onEventRecordStructure.addFieldStructure("TEMPERATURE", ConstantInteger.class);
		onEventRecordStructure.addFieldStructure("ATTITUDEX", ConstantInteger.class);
		onEventRecordStructure.addFieldStructure("ATTITUDEY", ConstantInteger.class);
		onEventRecordStructure.addFieldStructure("ATTITUDEZ", ConstantInteger.class);
		onEventRecordStructure.addFieldStructure("CRACKMETER1", ConstantInteger.class);
		onEventRecordStructure.addFieldStructure("CRACKMETER2", ConstantInteger.class);
		
		pFpcListener = new Thread(new PipeReader());
		pFpcListener.start();
	}

	/**
	 * Spacchetta il blob inviato dai livelli inferiori
	 */
	@Override
	protected void mngNormalMsg(byte[] parPayload) {
		if (acquisitionType == ON_EVENT) {
			manageOnEventSamplingPayload(ArrayUtility.toByteClassArray(parPayload));
		} else if (acquisitionType == CONTINUOUS) {
			manageContinuousSamplingPayload(ArrayUtility.toByteClassArray(parPayload));
		}
	}
	
	/**
	 * Gestisce i byte ricevuti dal mezzo fisico e li tratta come risultato di un sampling ad eventi 
	 * @param payload byte ricevuti da interpretare
	 */
	private void manageOnEventSamplingPayload(Byte[] payload) {
		// Converto da array di byte (blob) a struttura
		DSPicOnEventStatus status = new DSPicOnEventStatus();
		
		do {
			try {
				payload = DataConverter.fromByteArray(status, payload);
			} catch(IllegalArgumentException e) {
				System.out.println("Errore durante la conversione da array di byte a struttura: " + e.getMessage());
				break;
			}
			
			// Inserisco dati in una struttura record e li invio verso le pipe in uscita
			ED eventData;
			for (int i = 0; i < status.getData().getEventDataCount(); i ++) {
				// Creo il record ed inserisco i dati a campionamento lento
				Record record = new Record(onEventRecordStructure);
				record.setField("ACQUISITION_TYPE", new ConstantInteger(acquisitionType));
				record.setField("ID_NODO", new ConstantID(pDevNumber));
				record.setField("NUM_EVENTS", new ConstantInteger(status.getData().getNumEvents()));
				record.setField("TEMPERATURE", new ConstantInteger(status.getLowSamplingRateData().getTemperature()));
				record.setField("ATTITUDEX", new ConstantInteger(status.getLowSamplingRateData().getAttitudeX()));
				record.setField("ATTITUDEY", new ConstantInteger(status.getLowSamplingRateData().getAttitudeY()));
				record.setField("ATTITUDEZ", new ConstantInteger(status.getLowSamplingRateData().getAttitudeZ()));
				record.setField("CRACKMETER1", new ConstantInteger(status.getLowSamplingRateData().getCrackMeter1()));
				record.setField("CRACKMETER2", new ConstantInteger(status.getLowSamplingRateData().getCrackMeter2()));
				
				// Inserisco i dati dell'accellerometro
				eventData = status.getData().getEventData()[i];
				record.setField("TIMESTAMP", new ConstantTimestamp(eventData.getTimestamp()));
				record.setField("FLAGS", new ConstantInteger(eventData.getFlags()));
				record.setField("SAMPLES", new ConstantInteger(eventData.getSamples()));
				record.setField("MAGNITUDEX", new ConstantInteger(eventData.getMagnitudeX()));
				record.setField("MAGNITUDEY", new ConstantInteger(eventData.getMagnitudeY()));
				record.setField("MAGNITUDEZ", new ConstantInteger(eventData.getMagnitudeZ()));
				record.setField("NOISEX", new ConstantInteger(eventData.getNoiseX()));
				record.setField("NOISEY", new ConstantInteger(eventData.getNoiseY()));
				record.setField("NOISEZ", new ConstantInteger(eventData.getNoiseZ()));
				record.setField("PEAKX", new ConstantInteger(eventData.getPeakX()));
				record.setField("PEAKY", new ConstantInteger(eventData.getPeakY()));
				record.setField("PEAKZ", new ConstantInteger(eventData.getPeakZ()));
				record.setField("DATA", new ConstantBlob(Arrays.copyOfRange(status.getData().getEventDataSamples(), 
						eventData.getStart()*2, eventData.getStop()*2)));
				
				enqueueRecord(record);
			}
		} while(payload.length > sizeOnEvent);
	}
	
	/**
	 * Gestisce i byte ricevuti dal mezzo fisico e li tratta come risultato di un sampling continuo 
	 * @param payload byte ricevuti da interpretare
	 */
	private void manageContinuousSamplingPayload(Byte[] payload) {
		// Converto da array di byte (blob) a struttura
		DSPicOnContinuousStatus status = new DSPicOnContinuousStatus(); 
		
		// Riempio il record e lo mando al DataCollector
		do {
			try {
				payload = DataConverter.fromByteArray(status, payload);
			} catch(IllegalArgumentException e) {
				System.out.println("Errore durante la conversione da array di byte a struttura: " + e.getMessage());
				break;
			}
			
			Record record = new Record(continuousRecordStructure);
			record.setField("ACQUISITION_TYPE", new ConstantInteger(acquisitionType));
			record.setField("ID_NODO", new ConstantID(pDevNumber));
			record.setField("DATA", new ConstantBlob(status.getData().getEventDataSamples()));
			record.setField("TIMESTAMP", new ConstantTimestamp());
			record.setField("TEMPERATURE", new ConstantInteger(status.getLowSamplingRateData().getTemperature()));
			record.setField("ATTITUDEX", new ConstantInteger(status.getLowSamplingRateData().getAttitudeX()));
			record.setField("ATTITUDEY", new ConstantInteger(status.getLowSamplingRateData().getAttitudeY()));
			record.setField("ATTITUDEZ", new ConstantInteger(status.getLowSamplingRateData().getAttitudeZ()));
			record.setField("CRACKMETER1", new ConstantInteger(status.getLowSamplingRateData().getCrackMeter1()));
			record.setField("CRACKMETER2", new ConstantInteger(status.getLowSamplingRateData().getCrackMeter2()));
			
			enqueueRecord(record);
		} while(payload.length > sizeOnContinuous);
	}
	
	/**
	 * Esegue l'enqueue di un record su tutte le pipe in uscita dell'FPC
	 * @param record record da inserire nelle pipes
	 */
	private synchronized void enqueueRecord(Record record) {
		for (Pipe<Record> p : pDataCollectorPipeOut) {
			p.enqueue(record);
		}
	}
	
	/**
	 * Imposta i valori da aggiornare sul dispositivo
	 * @param parAttributeList lista dei valori da settare, inviati come coppie nome, valore
	 */
	public synchronized void setUpdateAttributes(ArrayList<PairValue<String, ? extends Constant>> parAttributeList) {
		AdapterFpcMessage message = new AdapterFpcMessage();
		try {
			for (PairValue<String, ? extends Constant> p : parAttributeList) {
				invokeSetter(p.getFirst(), p.getSecond(), pMRP);
			}
			message.setPayload(ArrayUtility.toPrimitiveByteArray(DataConverter.toByteArray(pMRP)));
			pFpcPipeOut.enqueue(message);
			System.out.println("Parametri impostati!");
		} catch(IllegalArgumentException e) {
			System.out.println("Erroree durante l'impostazione dei parametri da aggiornare: " + e.getMessage());
		}
	}
	
	/**
	 * Invoca il setter di un particolare campo della struttura dati dei parametri
	 * @param fieldName nome del campo da settare
	 * @param parameter valore da settare nel campo
	 * @param object oggetto contenente il campo da settare
	 * @throws IllegalArgumentException
	 */
	private void invokeSetter(String fieldName, Object parameter, Object object) throws IllegalArgumentException {
		
		Object[] parameters = new Object[1];
		Class<?>[] methodArgsClass = new Class<?>[1];
		
		String methodName = "set" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
		
		if (parameter.getClass() == ConstantInteger.class) {
			methodArgsClass[0] = int.class;
			parameters[0] = ((ConstantInteger)parameter).getValueInt();
		} else if (parameter.getClass() == ConstantVectorInteger.class) {
			methodArgsClass[0] = int[].class;
			parameters[0] = ((ConstantVectorInteger)parameter).getValueVec();
		}
		
		try {
			Method method = MRP.class.getMethod(methodName, methodArgsClass);
			
			method.invoke(object, parameters);
			
		}  catch (SecurityException e) {
			throw new IllegalArgumentException("L'oggetto di tipo " + object.getClass().getName()
					+ " passato come parametro non permette l'accesso al campo"
					+ fieldName + ". Controllare che sia stato dichiarato come pubblico!");
			
		} catch (NoSuchMethodException e) {
			throw new IllegalArgumentException("L'oggetto di tipo " + object.getClass().getName()
					+ " passato come parametro non ha metodo setter per il campo "+ fieldName + "!");
			
		} catch (IllegalAccessException e) {
			throw new IllegalArgumentException("L'oggetto di tipo " + object.getClass().getName() 
					+ " passato come parametro non ha setter pubblico per il campo" + fieldName + "!");
			
		} catch (InvocationTargetException e) {
			throw new IllegalArgumentException("Il metodo setter per il campo"
					+ fieldName + " del tipo " + object.getClass().getName() + 
					"ha generato un'eccezione! Controllare la sua implementazione");
		}
	}
	
	public synchronized Pipe<Record> registerQuery(ArrayList<String> parAttrbiutesNames) {
		// TODO memorizzare gli attributi da leggere sul dispositivo (sub set degli attributi "totali")
		// in modo da non dover restituire l'intero record a tutti
		Pipe<Record> tmpPipe = new Pipe<Record>("DSPicFPC " + pDevNumber + "_" + 
				pDataCollectorPipeOut.size() + " DataCollectorPipeOut");
		tmpPipe.start();
		pDataCollectorPipeOut.add(tmpPipe);
		return tmpPipe;
	}

	@Override
	public boolean setSamplingTime(long parTime, OnUnsupportedSampleRateOptions parBehaviour) {
		return true;
	}
	
	/**
	 * Invia un null su tutte le pipe in uscita in modo che gli
	 * oggetti in attesa di dati dalle Pipe possano terminare
	 */
	private synchronized void stopRegisteredListeners() {
		for(Pipe<?> p : pDataCollectorPipeOut) {
			p.enqueue(null);
		}
	}
	
	/**
	 * Inner class per la gestione della lettura sulla pipe di ingresso
	 * @author Marco Fortunato
	 */
	private class PipeReader implements Runnable {
		
		public void run() {
			Waitable<Pipe<AdapterFpcMessage>> waitable;
			Pipe<AdapterFpcMessage> pipe;
			AdapterFpcMessage message;
			
			while(true) {
				waitable = pFpcPipeInWaiter.waitNext();
				pipe = waitable.getParentComponent();
				message = pipe.dequeue();
				
				if (message != null) {
				 	mngNormalMsg(message.getPayload());
				} else {
					pFpcPipeInWaiter.removeWaitable(waitable);
					pipe.stop();
					stopRegisteredListeners();
					break;
				}
			}
			System.out.println("DSPicFPC_" + pDevNumber + " terminated!");
		}
	}
}